/**
 *  SVG ZoomNPan library 1.0
 *  ========================
 *
 * @param zoomElt object SVGSVGElement the root SVG element. If called from the SVG document to zoom, the argument can be omitted.
 * @param params  object Configuration parameters
 *                Available options:
 *                zoomCallback  Function called after zoom is done
 *                preZoomCallback Function called before zoom
 *                fitViewport boolean Specify if the image initially needs to be scaled to the size of the viewport, and not displayed
 *                                       at the size it was designed.
 *                fitBig boolean If  the designed size of the image is larger than the viewport, should we fit the image into the viewport?
 *                               Makes sense only when fitViewport is false
 */
 
var ZoomNPan = function (zoomElt,  params) {

	//private members
	var _currentState; 
	var _initialized;
	var _options;
	var _this;
	var _zoomElt;
	var SVGNS = "http://www.w3.org/2000/svg";

	//public members
	this.currentState;
	this.options;
	this.zoomCallback =  function(){};
	this.preZoomCallback = function(){};
    this.fitViewport = false;
    this.fitBig = true;

	if (params){
        if (params.zoomCallback) { this.zoomCallback = params.zoomCallback}
        if (params.preZoomCallback) { this.preZoomCallback = params.preZoomCallback}
        if (typeof params.fitViewport != 'undefined') { this.fitViewport = params.fitViewport }
        if (typeof params.fitBig != 'undefined') { this.fitBig = params.fitBig }
    }


	// ctor
	function ZoomNPan(zoomElt){
		// Guard parameters
		if (zoomElt == undefined) {
			zoomElt = window.document.documentElement;
		}
		if(zoomElt != '[object SVGSVGElement]') {
			window.console.log(zoomElt);
			window.console.log('ZoomNPan: Argument passed is not an SVG element');
			return;
		}
		
		// Set defaults/private members
        // TODO make options configurable from arguments
		_options = {
			autoRun: true,
			autoFitViewport: true, 
			zoomScale: 1.2,
			fastZoomMultiplier: 5,
			allowHotkeys: false,
			toggleZoomKeyCode: 27,     /* Esc */
			zoomInKeyCode: 61,         /* + */
			zoomOutKeyCode: 45,        /* - */
			resetZoomKeyCode: 48,      /* 0 */
			wireframeModeKeyCode: 119, /* w */
			// url() cursors don't work in opera =<11 (=>?), though it claims it supports 	
			cursorGrab: ' url(""), move',
			cursorHand: ' url(""),  default'
		};
		
		_currentState = {
			state: 'none',
			panX: 0,
			panY: 0,
			translateX: 0,
			translateY: 0,
			oldTranslateX: 0,
			oldTranslateY: 0,
			zoomAllowed: _options.autoRun,
			timePrev: 0,
			wireframeModeOn: false
		};
		
		_this    = this;
		_zoomElt = zoomElt;
		this.zoomElt      = _zoomElt;
		this.currentState = _currentState;
		this.options      = _options;
		this.origWidth    = zoomElt.getAttributeNS(null, "width");
		this.origHeight   = zoomElt.getAttributeNS(null, "height");
		this.origWidthPx  = zoomElt.width.animVal.value || zoomElt.getBBox().width;
		this.origHeightPx = zoomElt.height.animVal.value || zoomElt.getBBox().height;
		this.origViewBox  = zoomElt.getAttributeNS(null, "viewBox");
		this.origViewBoxO = JSON.parse(JSON.stringify(zoomElt.viewBox.animVal));
		this.origX        = zoomElt.getAttributeNS(null, "x");
		this.origY        = zoomElt.getAttributeNS(null, "y");

		addEvents();
		if (_options.autoFitViewport) {
            this.preZoomCallback();
            this.makeViewBox(_this.fitViewport, _this.fitBig)
        }

		_zoomElt.unsuspendRedrawAll();
		_initialized = true;
	}
	
	// Public methods
	
	// Global killswitch
	this.toggleZoomNPan = function (){
		
		_currentState.zoomAllowed = !_currentState.zoomAllowed;
		
		// Reset previous state and return
		if (!_currentState.zoomAllowed){
			_this.resetState();
			return
		}	
	};
	
	this.turnOnZoomNPan = function (){
		_currentState.zoomAllowed = true;
	};
	
	this.turnOffZoomNPan = function (){
		_currentState.zoomAllowed = false;
		_this.resetState();
	};
	
	this.zoomIt = function (delta) {
		ensureControl();
		var timeNow = Date.now();
		if (timeNow - _currentState.timePrev > 0) { // skip unnecessary redraw
			// don't let the image scale too small
			if (delta < 0 && _zoomElt.currentScale < 0.005) return;
			
			var cur = _currentState;
			var ZE  = _zoomElt;
			var oldScale = ZE.currentScale;
			var scaleFactor = Math.pow(1 + _options.zoomScale, delta);
			
			// Remember translate before zooming
			cur.oldTranslateX = ZE.currentTranslate.x;
			cur.oldTranslateY = ZE.currentTranslate.y;
			// Apply zoom
			ZE.currentScale *= scaleFactor;
			// Adjust position
			var vp_width = ZE.clientWidth;
			var vp_height = ZE.clientHeight;
			// Very complicated calculations :)
			// Borrowed from http://jwatt.org/svg/tests/zoom-and-pan-controls.svg
			ZE.currentTranslate.x = vp_width/2  - (ZE.currentScale/oldScale) * (vp_width/2  - cur.oldTranslateX);
			ZE.currentTranslate.y = vp_height/2 - (ZE.currentScale/oldScale) * (vp_height/2 - cur.oldTranslateY);
			cur.timePrev = timeNow;
			this.zoomCallback(this);
		 }
	};
	
	//	animates a 3x zoom over 10 steps
	this.animateZoom1x = function() {
		var aniDur   = 300; //ms
		var fps = 20;
		var aniSteps = aniDur / 1000 / fps;
		var curAniStep  = 0;
		var aniScaleStepVal = 0;
		var aniTrXStepVal = 0;
		var aniTrYStepVal = 0;
		var aniInterval = aniDur / 1000 / fps;

		aniScaleStepVal = (1 - _zoomElt.currentScale) / aniSteps;
		aniTrXStepVal = (1 - _zoomElt.currentTranslate.x) / aniSteps;
		aniTrYStepVal = (1 - _zoomElt.currentTranslate.y) / aniSteps;

		var deltaScale = (1 - _zoomElt.currentScale) / aniSteps;
		var deltaX = 0;
		var deltaY = 0;
/*		var aniElt = _zoomElt.document.createElementNS(SVGNS, "animate");
		//var aniElt = window.aniElt;
			aniElt.setAttributeNS(SVGNS, "attributeName", "x");
			aniElt.setAttributeNS(SVGNS, "attributeType", "XML");
			aniElt.setAttributeNS(SVGNS, "additive", "sum");
			aniElt.setAttributeNS(SVGNS, "fill", "freeze");
			aniElt.setAttributeNS(SVGNS, "from", 100);
			aniElt.setAttributeNS(SVGNS, "to", 1);
			aniElt.setAttributeNS(SVGNS, "begin", "indefinite");
			aniElt.setAttributeNS(SVGNS, "dur", aniDur + "ms");
			_zoomElt.appendChild(aniElt);
			aniElt.beginElement();*/

		var animation = window.setInterval(function(){
				if (++curAniStep < aniSteps) {
					if (_zoomElt.currentScale < 1) {
						_zoomElt.currentScale += aniScaleStepVal;
						_zoomElt.currentTranslate.x += aniTrXStepVal;
						_zoomElt.currentTranslate.y += aniTrYStepVal;
					}
					else if (_zoomElt.currentScale > 1) {
						_zoomElt.currentScale -= aniScaleStepVal;
						_zoomElt.currentTranslate.x -= aniTrXStepVal;
						_zoomElt.currentTranslate.y -= aniTrYStepVal;
					}
				}
				else if (curAniStep == aniSteps){
					// completely reset values
					_zoomElt.currentScale = 1;
					_zoomElt.currentTranslate.x = 0;
					_zoomElt.currentTranslate.y = 0;
				}
				else {
					window.clearInterval(animation);
					aniInterval = null;
				}
			}, 1);
/*		}
			window.setInterval(function(){
				if (++aniStep < aniSteps) {
				else if (aniStep == aniSteps){
					// completely reset values
					_zoomElt.currentScale = 1;
					_zoomElt.currentTranslate.x = 0;
					_zoomElt.currentTranslate.y = 0;

				}
				else {
					window.clearInterval(aniInterval);
					aniInterval = null;
				}

			}, 1)
		}*/
		this.zoomCallback(this);
	};

	this.zoom1x = function() {
		ensureControl();
		_zoomElt.currentScale = 1;//(1.0);
		_zoomElt.currentTranslate.x = 0;
		_zoomElt.currentTranslate.y = 0;
		this.zoomCallback(this);
	};

	this.zoomIn = function() {
		ensureControl();
		this.zoomIt(0.23)
	};

	this.zoomOut = function() {
		ensureControl();
		this.zoomIt(-0.23)
	};
	
	this.resetState = function () {
		ensureControl();
		//if (!_currentState.zoomAllowed){
			_zoomElt.style.cursor = 'default';
			_currentState.oldTranslateX = 0;
			_currentState.oldTranslateY = 0;
			_currentState.state = 'none';
			this.zoomCallback(this);
		//}
	};

    /**
     * Determines the viewBox values depending on the size of the image
     * and options of initial size of it
     *
     * @param fitViewport boolean optional Specify if the image initially needs to be displayed
     *        at the size it was designed, and not scaled to the size of the viewport.
     * @param fitBig boolean optional If  the designed size of the image is larger
     *        than the viewport, should we fit the image into the viewport?
     * @param recalculate
     *
     */

	this.makeViewBox = function (fitViewport, fitBig, recalculate){
        var fv, fb;
        typeof fitViewport != 'undefined' ? fv = fitViewport : fv = _this.fitViewport;
        typeof fitBig != 'undefined' ? fb = fitBig : fb = _this.fitBig;

        var doc     = _zoomElt;

        //TODO check doc.viewport in FF
		var w       = parseFloat(doc.viewport.width) || 0;
		var h       = parseFloat(doc.viewport.height) || 0;
		var wAttr   = doc.getAttributeNS(null, "width") || 0;
		var hAttr   = doc.getAttributeNS(null, "height") || 0;
		var vB      = doc.viewBox.animVal;//SVGRoot.getAttributeNS(null, "viewBox");
        var vpW     = doc.clientWidth;
        var vpH     = doc.clientHeight;
        var newvbX  = doc.x && doc.x.animVal && doc.x.animVal.value || 0;
        var newvbY  = doc.y && doc.y.animVal && doc.y.animVal.value || 0;
        var newvbW  = 0;
        var newvbH  = 0;
        if (recalculate){
            wAttr  = _this.origWidth;
            hAttr  = _this.origHeight;
            vB     = _this.origViewBoxO;
            newvbX = _this.origX;
            newvbX = _this.origY;
            //console.log(_this.origViewBoxO)
        }

        // if there was viewbox just reassign the values
        if(vB.width){
            newvbX = vB.x;
            newvbY = vB.y;
            newvbW = vB.width;
            newvbH = vB.height;
        }
        // First we need to get the viewbox values
	    // If there are NO width & height & viewbox try to guess them
		if (!wAttr && !hAttr && (vB && !vB.width)) {
			var BBox = doc.getBBox();
			newvbX = BBox.x;
			newvbY = BBox.y;
			newvbW = BBox.width;
			newvbH = BBox.height;
		}
		// If there IS width or height and NO viewBox,
		// generate viewbox based on them
		else if ((wAttr || hAttr) && !vB.width) {
            //animVal.value is in pixels, so it does all conversions from mm/inches
            newvbW = doc.width.animVal.value || w;
            newvbH = doc.height.animVal.value || h;

            // If W | H are %
            if(wAttr.indexOf("%") != -1) {
                newvbW = doc.width.animVal.valueInSpecifiedUnits / 100 * vpW;
            }
            if( hAttr.indexOf("%") != -1) {
                newvbH = doc.height.animVal.valueInSpecifiedUnits / 100 * vpH;
            }
		}

        if (!fitViewport) {
            //new width
            if (newvbW < vpW){
                newvbX = parseInt(vB.x - (vpW - newvbW)/2);
                newvbW = vpW;
            }
            else if (!fitBig){
                newvbW = vpW;
            }
            // new height
            if (newvbH < vpH){
                newvbY = parseInt(vB.y - (vpH - newvbH)/2);
                newvbH = vpH;
            }
            else if (!fitBig){
                newvbH = vpH;
            }
        }

        vB.x      = newvbX;
        vB.y      = newvbY;
        vB.width  = newvbW;
        vB.height = newvbH;

        _zoomElt.removeAttributeNS(null, 'width');
        _zoomElt.removeAttributeNS(null, 'height');

        _this.zoomCallback(this);
	};


    // @param turnOn bolean pass if need to turn on, not just toggle
	this.wireframeMode = function (turnOn) {
		var wireStyleID = "ZoomNPan_wireframeMode";
		var WMIsOn = _zoomElt.getElementById(wireStyleID);
		if (WMIsOn && turnOn) return;// Do nothing if it's already ON
        
		if (WMIsOn) {
			WMIsOn.parentNode.removeChild(WMIsOn)
		}
		else {
			var wireCSS = "\
				svg * { \
					fill: none !important;\r\n\
					opacity: 1 !important;\r\n\
					color: transparent !important;\r\n\
					pointer-events: stroke !important;\r\n\
					stroke: #000 !important;\r\n\
					stroke-dasharray: 0 !important;\r\n\
					stroke-opacity: 1 !important;\r\n\
					stroke-width: 1px !important;\r\n\
					vector-effect: non-scaling-stroke !important;\r\n\
					filter: none !important;\r\n\
					shape-rendering:crispEdges !important;\r\n\
				} ";
			var wireStyle = window.document.createElementNS(SVGNS, 'style');
			wireStyle.setAttributeNS(SVGNS, 'id', wireStyleID);
			wireStyle.setAttributeNS(SVGNS, 'type', 'text/css');
			wireStyle.textContent = wireCSS;
			_zoomElt.appendChild(wireStyle);
		}
	};

    // s - value to parse, svgroot - svg element to get screenPixelToMillimeter from
/*	this.getLengthValue = function(s, svgroot) {
        var svgel = svgroot || _zoomElt;
        var mminpx = svgel.screenPixelToMillimeterX;
        var rxLength = /^(\d+\.\d*|\d*\.\d+|\d+)(px|in|cm|mm)?$/;

        if (s != null) {
            var m = rxLength.exec(s);
            //setStatus(m);
            if (m != null){
                var w = parseFloat(m[1]);
                if (m[2] == "in")
                    w *= 96;
                else if (m[2] == "cm")
                    w = w / mminpx * 10;
                else if (m[2] == "mm")
                    w = w / mminpx;
                return w;
            }
        }
        return null;
    };*/

    
	// Private methods
	
	function addEvents(){
		_zoomElt.addEventListener('mousedown',  handleMouseDown, false);
		_zoomElt.addEventListener('mouseup',    handleMouseUp, false);
		_zoomElt.addEventListener('mousemove',  handleMouseMove, false);
		_zoomElt.addEventListener('mousewheel', handleMouseWheel, false);
		_zoomElt.addEventListener('keypress',   handleKeyPress, false);
		//_zoomElt.addEventListener('DOMMouseScroll', handleMouseWheel, false);// mousewheel for Firefox
	}
	
	function cancelEvent(evt){
		if (!evt) return;
		if (evt.preventDefault){
			evt.preventDefault();
		}
		evt.returnValue = false;
	}
	
	function ensureControl(){
		if (!_initialized) throw new Error("ZoomNPan: control has not been initialized.");
	}

	
	// Events
	function handleMouseMove(evt) { 
		if (_currentState.zoomAllowed){
			cancelEvent(evt);

			if(_currentState.state == 'pan') {
				// Pan mode
				// get difference to previous position
				_currentState.translateX = evt.x - _currentState.panX;
				_currentState.translateY = evt.y - _currentState.panY;

				evt.currentTarget.currentTranslate.x += _currentState.translateX;
				evt.currentTarget.currentTranslate.y += _currentState.translateY;
					
				// Remember previous position
				_currentState.panX = evt.x;
				_currentState.panY = evt.y;
			}
		}
	}
	
	function handleMouseDown(evt) {
		if (_currentState.zoomAllowed) {
			if (evt.button != 0) return; // We want only left click
			cancelEvent(evt);
			if(evt.currentTarget.tagName == "svg") {
				// Pan mode
				_currentState.state = 'pan';
				_zoomElt.style.cursor = _options.cursorGrab;
				_currentState.panX = evt.x;
				_currentState.panY = evt.y;
			}
		}
	}
	
	function handleMouseUp(evt) {
		//alert('mouse released')
		if (_currentState.zoomAllowed){
			cancelEvent(evt);
			if (_currentState.state == 'pan') {
				// Quit pan mode
				_zoomElt.style.cursor = _options.cursorHand;//'default'
				_currentState.state = '';
			}
		}
	}
	
	function handleMouseWheel(evt) {
		if (_currentState.zoomAllowed){
			cancelEvent(evt);
			var delta;
			if (evt.wheelDelta) {
				delta = evt.wheelDelta / 360; // Webkit, Opera
			}
			else{
				delta = evt.detail / -9; // Mozilla, Opera
			}
			if (evt.altKey){
				delta *= _options.fastZoomMultiplier;
			}
			_this.zoomIt(delta);
		}
	}
	
	
	function handleKeyPress(evt) {
		//	opera.postError(evt.keyCode)
		if (!_options.allowHotkeys){
			return
		}
		
		if (evt.keyCode == _options.toggleZoomKeyCode){
			_this.toggleZoomNPan()
		}
		
		if (_currentState.zoomAllowed){
				
			 if (evt.keyCode == _options.zoomInKeyCode){
				cancelEvent(evt);
				_this.zoomIn();
			}
			
			else if (evt.keyCode == _options.zoomOutKeyCode){
				cancelEvent(evt);
				_this.zoomOut();
			}
			else if (evt.keyCode == _options.resetZoomKeyCode){
				cancelEvent(evt);
				_this.zoom1x();
				//_this.animateZoom1x();
			}
			else if (evt.keyCode == _options.wireframeModeKeyCode){
				cancelEvent(evt);
				_this.wireframeMode();
				//_this.animateZoom1x();
			}
		}
	}
	
	function handleMouseOut(evt){
		if (_currentState.zoomAllowed){
		// not used, buggy
		// Handle mouse out event
			cancelEvent(evt);
			if (_currentState.state == 'pan') {
				// Quit pan mode
				_currentState.state = '';
			}
		}
	}

    function extend(obj, extObj) {
        if (arguments.length > 2) {
            for (var a = 1; a < arguments.length; a++) {
                extend(obj, arguments[a]);
            }
        } else {
            for (var i in extObj) {
                obj[i] = extObj[i];
            }
        }
        return obj;
    }
	// Call ctor, preserving instantiation scope;
	ZoomNPan.apply(this, arguments);
	
	
};
